package ArithmeticObjects;

import java.io.IOException;

import utils.ByteTree;

/**
 * Representing a product ring element
 * @author Idan
 */
public class ProductRingElement extends Element {

	private Ring Zq;
	private ProductRingElement left;
	private ProductRingElement right;
	
	/**
	 * creates a ProductRingElement from the given ByteTree representing it 
	 * and a Ring indicating its order
	 * @param bt ByteTree representing the ProductRingElement
	 * @param Zq Ring indicating its order
	 * @throws Exception if the construction failed
	 */
	public ProductRingElement(ByteTree bt, Ring Zq) throws Exception
	{
		if (!(Zq.getOrder() instanceof LargeNumberObject))
			throw new Exception("Wrong class, Expected: LargeNumberObject, got: " + Zq.getOrder().getClass().getName());
		this.Zq = Zq;
		if (!bt.isNode())
		{
			value  = bt.getNumber().mod(Zq.getOrder().getValue());
			left = null;
			right = null;
		}
		else if (bt.getNumOfChildren() == 2)
		{
			left = new ProductRingElement(bt.getChild(0), Zq);
			right = new ProductRingElement(bt.getChild(1), Zq);
			value = null;
		}
		else
		{
			throw new Exception("Wrong number of children, Expected: 2 , got: " + bt.getNumOfChildren());
		}
	}
	
	/**
	 * creates a ProductRingElement from the given LargeNumberObject representing it 
	 * and a Ring indicating its order
	 * @param elem LargeNumberObject representing the ProductRingElement
	 * @param Zq Ring indicating its order
	 * @throws Exception if the construction failed
	 */
	public ProductRingElement(Element elem, Ring Zq) throws Exception
	{
		if (!(Zq.getOrder() instanceof LargeNumberObject))
			throw new Exception("Wrong class, Expected: LargeNumberObject, got: " + Zq.getOrder().getClass().getName());
		this.Zq = Zq;
		if (!(elem instanceof LargeNumberObject))
			throw new Exception("Wrong class, Expected: LargeNumberObject, got: " + elem.getClass().getName());
		value = elem.getValue();
		right = null;
		left = null;
	}
	
	/**
	 * creates a ProductRingElement from the two given ProductRingElements representing it (right and left children) 
	 * and a Ring indicating its order
	 * @param left ProductRingElement representing the left child of the current instance
	 * @param right ProductRingElement representing the right child of the current instance
	 * @param Zq Ring indicating its order
	 * @throws Exception if the construction failed
	 */
	public ProductRingElement(Element left, Element right, Ring Zq) throws Exception
	{
		if (!(Zq.getOrder() instanceof LargeNumberObject))
			throw new Exception("Wrong class, Expected: LargeNumberObject, got: " + Zq.getOrder().getClass().getName());
		this.Zq = Zq;
		if (!(left instanceof ProductGroupElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + left.getClass().getName());
		if (!(right instanceof ProductGroupElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + right.getClass().getName());
		this.right = (ProductRingElement) right;
		this.left = (ProductRingElement) left;
		value = null;
	}
	
	/**
	 * helper recursive function that adds the given ProductRingElement to the current on coordinate wise
	 * @param b1 ByteTree node/leaf of the current ProductRingElement   
	 * @param b2 ByteTree node/leaf of the given ProductRingElement
	 * @return the result of recursively adding both ByteTrees coordinate wise
	 * @throws Exception if the addition operation failed
	 */
	private Element addCoordinates(Element e1, Element e2) throws Exception {
		if ((e1.getValue() != null) && (e2.getValue() != null))
		{
			LargeNumber n = e1.getValue().add(e2.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((e1 instanceof ProductRingElement) && (e2 instanceof ProductRingElement))
		{
			ProductRingElement p1 = (ProductRingElement) e1;
			ProductRingElement p2 = (ProductRingElement) e2;
			Element leftRes = addCoordinates(p1.left, p2.left);
			Element rightRes = addCoordinates(p1.right, p2.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else 
		{
			throw new Exception("Cannot add Product ring elements of different structure");
		}
	}
	
	@Override
	public Element add(Element elementA, Element...elements ) throws Exception{
		if (elements.length > 0) throw new Exception("Wrong number of arguments, Product Ring Element can only add one element at a time");
		if (!(elementA instanceof ProductRingElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + elementA.getClass().getName());
		if (this.getOrder().compareTo(((ProductRingElement)elementA).getOrder()) != 0)
				throw new Exception("Cannot add Product ring elements of different orders");
		if ((this.getValue() != null) && (elementA.getValue() != null))
		{
			LargeNumber n = this.getValue().add(elementA.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((this.getValue() == null) && (elementA.getValue() == null))
		{
			ProductRingElement pA = (ProductRingElement) elementA;
			Element leftRes = addCoordinates(this.left, pA.left);
			Element rightRes = addCoordinates(this.right, pA.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else
		{
			throw new Exception("Cannot add Product ring elements of different structure");
		}
	}
	
	/**
	 * helper recursive function that subtracts the given ProductRingElement to the current on coordinate wise
	 * @param b1 ByteTree node/leaf of the current ProductRingElement   
	 * @param b2 ByteTree node/leaf of the given ProductRingElement
	 * @return the result of recursively subtracting both ByteTrees coordinate wise
	 * @throws Exception if the subtraction operation failed
	 */
	private Element subtractCoordinates(Element e1, Element e2) throws Exception {
		if ((e1.getValue() != null) && (e2.getValue() != null))
		{
			LargeNumber n = e1.getValue().subtract(e2.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((e1 instanceof ProductRingElement) && (e2 instanceof ProductRingElement))
		{
			ProductRingElement p1 = (ProductRingElement) e1;
			ProductRingElement p2 = (ProductRingElement) e2;
			Element leftRes = subtractCoordinates(p1.left, p2.left);
			Element rightRes = subtractCoordinates(p1.right, p2.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else 
		{
			throw new Exception("Cannot subtract Product ring elements of different structure");
		}
	}
	
	@Override
	public Element subtract(Element elementA) throws Exception{
		if (!(elementA instanceof ProductRingElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + elementA.getClass().getName());
		if (this.getOrder().compareTo(((ProductRingElement)elementA).getOrder()) != 0)
			throw new Exception("Cannot multiply Product ring elements of different orders");
		if ((this.getValue() != null) && (elementA.getValue() != null))
		{
			LargeNumber n = this.getValue().subtract(elementA.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((this.getValue() == null) && (elementA.getValue() == null))
		{
			ProductRingElement pA = (ProductRingElement) elementA;
			Element leftRes = subtractCoordinates(this.left, pA.left);
			Element rightRes = subtractCoordinates(this.right, pA.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else
		{
			throw new Exception("Cannot subtract Product ring elements of different structure");
		}
	}

	/**
	 * helper recursive function that multiplies the given ProductRingElement by the current on coordinate wise
	 * @param b1 ByteTree node/leaf of the current ProductRingElement   
	 * @param b2 ByteTree node/leaf of the given ProductRingElement
	 * @return the result of recursively multiplying both ByteTrees coordinate wise
	 * @throws Exception if the multiplication operation failed
	 */
	private Element multiplyCoordinates(Element e1, Element e2) throws Exception {
		if ((e1.getValue() != null) && (e2.getValue() != null))
		{
			LargeNumber n = e1.getValue().multiply(e2.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((e1 instanceof ProductRingElement) && (e2 instanceof ProductRingElement))
		{
			ProductRingElement p1 = (ProductRingElement) e1;
			ProductRingElement p2 = (ProductRingElement) e2;
			Element leftRes = multiplyCoordinates(p1.left, p2.left);
			Element rightRes = multiplyCoordinates(p1.right, p2.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else 
		{
			throw new Exception("Cannot multiply Product ring elements of different structure");
		}
	}
	
	@Override
	public Element multiply(Element elementA, Element...elements ) throws Exception{
		if (elements.length > 0) throw new Exception("Wrong number of arguments, Product ring Element can only multiply one element at a time");
		if (!(elementA instanceof ProductRingElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + elementA.getClass().getName());
		if (this.getOrder().compareTo(((ProductRingElement)elementA).getOrder()) != 0)
			throw new Exception("Cannot multiply Product ring elements of different orders");
		if ((this.getValue() != null) && (elementA.getValue() != null))
		{
			LargeNumber n = this.getValue().multiply(elementA.getValue()).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((this.getValue() == null) && (elementA.getValue() == null))
		{
			ProductRingElement pA = (ProductRingElement) elementA;
			Element leftRes = multiplyCoordinates(this.left, pA.left);
			Element rightRes = multiplyCoordinates(this.right, pA.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else
		{
			throw new Exception("Cannot multiply Product ring elements of different structure");
		}
	}

	/**
	 * helper recursive function that divides the given ProductRingElement by the current on coordinate wise
	 * @param b1 ByteTree node/leaf of the current ProductRingElement   
	 * @param b2 ByteTree node/leaf of the given ProductRingElement
	 * @return the result of recursively dividing both ByteTrees coordinate wise
	 * @throws Exception if the division operation failed
	 */
	private Element divideCoordinates(Element e1, Element e2) throws Exception {
		if ((e1.getValue() != null) && (e2.getValue() != null))
		{
			LargeNumber n = new LargeNumber(e1.getValue().multiply(e2.getValue().modInverse(Zq.getOrder().getValue()))).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((e1 instanceof ProductRingElement) && (e2 instanceof ProductRingElement))
		{
			ProductRingElement p1 = (ProductRingElement) e1;
			ProductRingElement p2 = (ProductRingElement) e2;
			Element leftRes = divideCoordinates(p1.left, p2.left);
			Element rightRes = divideCoordinates(p1.right, p2.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else 
		{
			throw new Exception("Cannot divide Product ring elements of different structure");
		}
	}
	
	@Override
	public Element divide(Element elementA) throws Exception{
		if (!(elementA instanceof ProductRingElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + elementA.getClass().getName());
		if (this.getOrder().compareTo(((ProductRingElement)elementA).getOrder()) != 0)
			throw new Exception("Cannot divide Product ring elements of different orders");
		if ((this.getValue() != null) && (elementA.getValue() != null))
		{
			LargeNumber n = new LargeNumber(this.getValue().multiply(elementA.getValue().modInverse(Zq.getOrder().getValue()))).mod(Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else if ((this.getValue() == null) && (elementA.getValue() == null))
		{
			ProductRingElement pA = (ProductRingElement) elementA;
			Element leftRes = divideCoordinates(this.left, pA.left);
			Element rightRes = divideCoordinates(this.right, pA.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
		else
		{
			throw new Exception("Cannot divide Product ring elements of different structure");
		}
	}
	
	/**
	 * helper recursive function that compares the given ProductRingElement to the current on coordinate wise
	 * @param b1 ByteTree node/leaf of the current ProductRingElement   
	 * @param b2 ByteTree node/leaf of the given ProductRingElement
	 * @return the result of recursively comparing both ByteTrees coordinate wise
	 * @throws Exception if the comparison operation failed
	 */
	private boolean nodeCompare(Element e1, Element e2) throws Exception {
		if ((e1.getValue() != null) && (e2.getValue() != null))
		{
			return (e1.getValue().compareTo(e2.getValue()) == 0);
		}
		else if ((e1 instanceof ProductRingElement) && (e2 instanceof ProductRingElement))
		{
			ProductRingElement p1 = (ProductRingElement) e1;
			ProductRingElement p2 = (ProductRingElement) e2;
			return nodeCompare(p1.left,p2.left) && nodeCompare(p1.right,p2.right);
		}
		else 
		{
			throw new Exception("Cannot compare Product ring elements of different structure");
		}
	}

	@Override
	public int compareTo(Element elementA) throws Exception {
		if (!(elementA instanceof ProductRingElement))
			throw new Exception("Wrong class, Expected: ProductRingElement, got: " + elementA.getClass().getName());
		if (this.getOrder().getValue().compareTo(((ProductRingElement)elementA).getOrder().getValue()) !=0)
			throw new Exception("Cannot compare Product ring elements of different orders");
		return nodeCompare(this, elementA) ? 0 : 1;
	}

	/**
	 * helper recursive function that raises the given ProductRingElement by the power of exponent
	 * @param bt ByteTree node/leaf of the current ProductRingElement
	 * @param exponent the exponent to raise by
	 * @return the result of recursively raising the ByteTree by exponent coordinate wise
	 * @throws Exception if the power operation failed
	 */
	private Element nodePower(Element e, Element exponent) throws Exception {
		if (e.getValue() != null)
		{
			LargeNumber n = e.getValue().modPow(exponent.getValue(), Zq.getOrder().getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else
		{
			ProductRingElement p = (ProductRingElement) e;
			Element leftRes = nodePower(p.left, exponent);
			Element rightRes = nodePower(p.right, exponent);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
	}
	
	@Override
	public Element power(Element exponent) throws Exception {
		if (this.getValue() != null)
		{
			LargeNumber n = new LargeNumber(this.getValue().modPow(exponent.getValue(), Zq.getOrder().getValue()));
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else
		{
			Element leftRes = nodePower(this.left, exponent);
			Element rightRes = nodePower(this.right, exponent);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}		
	}
	
	/**
	 * helper recursive function that replaces the current ProductRingElement with the additive 
	 * inverse coordinate wise
	 * @param bt ByteTree node/leaf of the current ProductRingElement
	 * @return the result of recursively replacing the ByteTree with its additive inverse coordinate wise
	 * @throws Exception if the inverse operation failed
	 */
	private Element inverseCoordinates(Element e) throws Exception {
		if (e.getValue() != null)
		{
			LargeNumber n = Zq.getOrder().getValue().subtract(e.getValue());
			return new ProductRingElement(new LargeNumberObject(n), Zq);
		}
		else
		{
			ProductRingElement p = (ProductRingElement) e;
			Element leftRes = inverseCoordinates(p.left);
			Element rightRes = inverseCoordinates(p.right);
			return new ProductRingElement(leftRes, rightRes, Zq);
		}
	}
	
	@Override
	public Element additiveInverse() throws Exception {
		return new ProductRingElement(inverseCoordinates(this),Zq);	
	}
	
	/**
	 * helper function that creates the string representation of the ProductRingElement
	 * @param bt ByteTree node/leaf of the current ProductRingElement
	 * @return the result of recursively appending the ByteTree string representation to its predecessors
	 */
	private String toStringCoordinates(Element e)
	{
		if (e.getValue() != null)
		{
			return e.getValue().toString();
		}
		else
		{
			ProductRingElement p = (ProductRingElement) e;
			return "left:\n" + toStringCoordinates(p.left)
					+ "\n" + 
					"right:\n" + toStringCoordinates(p.right)
					+ "\n";
		}
	}
	
	@Override
	public String toString() {
		return toStringCoordinates(this) + "\n";
	}

	/**
	 * @return the left node of the ProductRingElement
	 */
	public Element getLeftChild(){
		return left;
	}
	
	/**
	 * @return the right node of the ProductRingElement
	 */
	public Element getRightChild() {
		return right;
	}
	
	public Element getOrder()
	{
		return Zq.getOrder();
	}
	
	@Override
	public LargeNumber getValue() {
		return value;
	}
	
	@Override
	public ByteTree getByteTree() throws IOException{
		if (getValue()!=null)
		{
			return ByteTree.CreateFixedSizeLeaf(value.toByteArray(),(Zq.getOrder().getValue().bitLength()+7)/8);
		}
		else
		{
			ByteTree node = ByteTree.CreateNode();
			return node.AddMultiChildsToNode(left.getByteTree(), right.getByteTree());
		}
	}
}
